package org.luxor.commons.schedule.config;

import org.luxor.commons.schedule.constant.ScheduleModeStatus;
import org.luxor.commons.schedule.constant.ScheduleStatus;
import org.luxor.commons.schedule.entity.ScheduleJobEntity;
import org.luxor.commons.schedule.service.IScheduleJobService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.framework.AopInfrastructureBean;
import org.springframework.aop.framework.AopProxyUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.EmbeddedValueResolverAware;
import org.springframework.core.MethodIntrospector;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.lang.Nullable;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.scheduling.annotation.Schedules;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.util.StringValueResolver;

import java.lang.reflect.Method;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 注解定时任务收集器
 *
 * @author Mr.yan @date 2020/2/23
 */
public class ScheduledAnnotationBeanInitPostProcessor implements BeanPostProcessor, SmartInitializingSingleton, EmbeddedValueResolverAware, Ordered {
    private final Logger logger = LoggerFactory.getLogger(ScheduledAnnotationBeanInitPostProcessor.class);

    private final Set<Class<?>> nonAnnotatedClasses = Collections.newSetFromMap(new ConcurrentHashMap<>(64));

    @Nullable
    private StringValueResolver embeddedValueResolver;

    private final IScheduleJobService scheduleJobService;

    public ScheduledAnnotationBeanInitPostProcessor(IScheduleJobService scheduleJobService) {
        super();
        this.scheduleJobService = scheduleJobService;
    }

    @Override
    public void setEmbeddedValueResolver(StringValueResolver resolver) {
        this.embeddedValueResolver = resolver;
    }

    @Override
    public int getOrder() {
        return LOWEST_PRECEDENCE;
    }

    @Override
    public void afterSingletonsInstantiated() {
        // Remove resolved singleton classes from cache
        this.nonAnnotatedClasses.clear();
    }

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if (bean instanceof AopInfrastructureBean) {
            // Ignore AOP infrastructure such as scoped proxies.
            return bean;
        }
        Class<?> targetClass = AopProxyUtils.ultimateTargetClass(bean);
        if (!this.nonAnnotatedClasses.contains(targetClass)) {
            Map<Method, Set<Scheduled>> annotatedMethods = MethodIntrospector.selectMethods(targetClass,
                    (MethodIntrospector.MetadataLookup<Set<Scheduled>>) method -> {
                        Set<Scheduled> scheduledMethods = AnnotatedElementUtils.getMergedRepeatableAnnotations(
                                method, Scheduled.class, Schedules.class);
                        return (!scheduledMethods.isEmpty() ? scheduledMethods : null);
                    });
            if (annotatedMethods.isEmpty()) {
                this.nonAnnotatedClasses.add(targetClass);
            } else {
                // Non-empty set of methods
                annotatedMethods.forEach((method, scheduledMethods) ->
                        scheduledMethods.forEach(scheduled -> processScheduled(scheduled, method, beanName)));
            }
        }
        return bean;
    }

    private void processScheduled(Scheduled scheduled, Method method, String beanName) {
        Assert.isTrue(method.getParameterCount() == 0, "Only no-arg methods may be annotated with @Scheduled");
        boolean processedSchedule = false;
        String errorMessage = "Exactly one of the 'cron', 'fixedDelay(String)', or 'fixedRate(String)' attributes is required";

        // Determine initial delay (程序启动后延迟{initialDelay}秒执行定时器)
        long initialDelay = scheduled.initialDelay();
        String initialDelayString = scheduled.initialDelayString();
        if (StringUtils.hasText(initialDelayString)) {
            Assert.isTrue(initialDelay < 0, "Specify 'initialDelay' or 'initialDelayString', not both");
            if (this.embeddedValueResolver != null) {
                initialDelayString = this.embeddedValueResolver.resolveStringValue(initialDelayString);
            }
            if (StringUtils.hasLength(initialDelayString)) {
                initialDelay = parseDelayAsLong(initialDelayString);
            }
        }

        // Check cron expression ( Cron表达式 )
        String cronExpression = scheduled.cron();
        if (StringUtils.hasText(cronExpression)) {
            String zone = scheduled.zone();
            if (this.embeddedValueResolver != null) {
                cronExpression = this.embeddedValueResolver.resolveStringValue(cronExpression);
                zone = this.embeddedValueResolver.resolveStringValue(zone);
            }
            if (StringUtils.hasLength(cronExpression)) {
                Assert.isTrue(initialDelay == -1, "'initialDelay' not supported for cron triggers");
                processedSchedule = true;
                TimeZone timeZone;
                if (StringUtils.hasText(zone)) {
                    timeZone = StringUtils.parseTimeZoneString(zone);
                } else {
                    timeZone = TimeZone.getDefault();
                }
                // 保存Cron表达式任务
                creatCronExpressionJob(beanName, method.getName(), cronExpression);
                if (logger.isDebugEnabled()) {
                    logger.debug("JobPoint: {}.{}(); cronExpression:{} timeZone:{}", method.getDeclaringClass().getName(), method.getName(), cronExpression, timeZone);
                }
            }
        }

        if (initialDelay < 0) {
            initialDelay = 0;
        }

        // Check fixed delay (【阻塞循环】上一次执行完毕之后{fixedDelay}秒再执行。)
        long fixedDelay = scheduled.fixedDelay();
        if (fixedDelay >= 0) {
            Assert.isTrue(!processedSchedule, errorMessage);
            processedSchedule = true;

            // 保存阻塞循环任务(fixed delay)
            creatFixedDelayJob(beanName, method.getName(), initialDelay, fixedDelay);
            if (logger.isDebugEnabled()) {
                logger.debug("JobPoint: {}.{}(); initialDelay:{} / fixedDelay:{}", method.getDeclaringClass().getName(), method.getName(), initialDelay, fixedDelay);
            }
        }
        String fixedDelayString = scheduled.fixedDelayString();
        if (StringUtils.hasText(fixedDelayString)) {
            if (this.embeddedValueResolver != null) {
                fixedDelayString = this.embeddedValueResolver.resolveStringValue(fixedDelayString);
            }
            if (StringUtils.hasLength(fixedDelayString)) {
                Assert.isTrue(!processedSchedule, errorMessage);
                processedSchedule = true;
                fixedDelay = parseDelayAsLong(fixedDelayString);

                // 保存阻塞循环任务(fixed delay)
                creatFixedDelayJob(beanName, method.getName(), initialDelay, fixedDelay);
                if (logger.isDebugEnabled()) {
                    logger.debug("JobPoint: {}.{}(); initialDelay:{} / fixedDelay:{}", method.getDeclaringClass().getName(), method.getName(), initialDelay, fixedDelay);
                }
            }
        }

        // Check fixed rate (【非阻塞循环】上一次执行时间点之后5秒执行。)
        long fixedRate = scheduled.fixedRate();
        if (fixedRate >= 0) {
            Assert.isTrue(!processedSchedule, errorMessage);
            processedSchedule = true;

            // 保存非阻塞循环任务(fixed rate)
            creatFixedRateJob(beanName, method.getName(), initialDelay, fixedRate);
            if (logger.isDebugEnabled()) {
                logger.debug("JobPoint: {}.{}(); initialDelay:{} / fixedRate:{}", method.getDeclaringClass().getName(), method.getName(), initialDelay, fixedRate);
            }
        }
        String fixedRateString = scheduled.fixedRateString();
        if (StringUtils.hasText(fixedRateString)) {
            if (this.embeddedValueResolver != null) {
                fixedRateString = this.embeddedValueResolver.resolveStringValue(fixedRateString);
            }
            if (StringUtils.hasLength(fixedRateString)) {
                Assert.isTrue(!processedSchedule, errorMessage);
                processedSchedule = true;
                fixedRate = parseDelayAsLong(fixedRateString);

                // 保存非阻塞循环任务(fixed rate)
                creatFixedRateJob(beanName, method.getName(), initialDelay, fixedRate);
                if (logger.isDebugEnabled()) {
                    logger.debug("JobPoint: {}.{}(); initialDelay:{} / fixedRate:{}", method.getDeclaringClass().getName(), method.getName(), initialDelay, fixedRate);
                }
            }
        }

        // Check whether we had any attribute set
        Assert.isTrue(processedSchedule, errorMessage);
    }

    private static long parseDelayAsLong(String value) {
        if (value.length() > 1 && (isP(value.charAt(0)) || isP(value.charAt(1)))) {
            return Duration.parse(value).toMillis();
        }
        return Long.parseLong(value);
    }

    private static boolean isP(char ch) {
        return (ch == 'P' || ch == 'p');
    }

    private void creatCronExpressionJob(String beanName, String methodName, String cronExpression) {
        ScheduleJobEntity scheduleJob = new ScheduleJobEntity();
        scheduleJob.setBeanName(beanName);
        scheduleJob.setMethodName(methodName);
        scheduleJob.setSchedulingMode(ScheduleModeStatus.CORE_EXPRESSION.getValue());
        scheduleJob.setInitialDelay(-1L);
        scheduleJob.setCronExpression(cronExpression);
        scheduleJob.setStatus(ScheduleStatus.NORMAL.getValue());
        scheduleJob.setCreateTime(new Date());
        scheduleJobService.saveJob(scheduleJob);
    }

    private void creatFixedDelayJob(String beanName, String methodName, Long initialDelay, Long fixedDelay) {
        ScheduleJobEntity scheduleJob = new ScheduleJobEntity();
        scheduleJob.setBeanName(beanName);
        scheduleJob.setMethodName(methodName);
        scheduleJob.setSchedulingMode(ScheduleModeStatus.FIXED_DELAY.getValue());
        scheduleJob.setInitialDelay(initialDelay);
        scheduleJob.setFixedDelay(fixedDelay);
        scheduleJob.setStatus(ScheduleStatus.NORMAL.getValue());
        scheduleJob.setCreateTime(new Date());
        scheduleJobService.saveJob(scheduleJob);
    }

    private void creatFixedRateJob(String beanName, String methodName, Long initialDelay, Long fixedRate) {
        ScheduleJobEntity scheduleJob = new ScheduleJobEntity();
        scheduleJob.setBeanName(beanName);
        scheduleJob.setMethodName(methodName);
        scheduleJob.setSchedulingMode(ScheduleModeStatus.FIXED_RATE.getValue());
        scheduleJob.setInitialDelay(initialDelay);
        scheduleJob.setFixedRate(fixedRate);
        scheduleJob.setStatus(ScheduleStatus.NORMAL.getValue());
        scheduleJob.setCreateTime(new Date());
        scheduleJobService.saveJob(scheduleJob);
    }
}
